﻿// jQuery plugin for Microsoft Tag

// Copyright (c) 2011 Brad More, http://www.bonedrymartini.com

// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:

// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

(function ($) {

    $.tag = function (element, options) {

        this.options = {};

        this.init = function (element, options) {
            this.options = $.extend({}, $.tag.defaultOptions, options);
            this.options = $.metadata ? $.extend({}, this.options, element.metadata()) : this.options;
            render(element, this.options);
        };

        this.init(element, options);
    };

    $.fn.tag = function (options) {
        return this.each(function () {
            (new $.tag($(this), options));
        });
    };

    // private vars and settings
    var baseSettings = {
        whiteBorder: 7,
        blackFrameTop: 3,
        blackFrameSides: 5,
        blackFrameBottom: 7,
        rowSpacerHeight: 2,
        rowOverlayOverFrameSides: 2,
        triangleHeight: 7
    };

    var colorSettings = {
        units: 71,
        frameHeightAndWidth: 57,
        rowSpacerWidth: 51,
        rowWidth: 47
    };

    var blackAndWhiteSettings = {
        units: 89,
        frameHeightAndWidth: 75,
        rowSpacerWidth: 69,
        rowOverlayOverFrameSides: 2,
        rowWidth: 65
    };

    // render background
    function background(context, settings, options, callback) {

        // if there's a background color specififed
        if (options.backgroundColor) {

            context.fillStyle = options.backgroundColor;
            context.fillRect(
                (settings.whiteBorder + settings.blackFrameSides) * settings.pixelsPerUnit,
                (settings.whiteBorder + settings.blackFrameTop) * settings.pixelsPerUnit,
                settings.rowWidth * settings.pixelsPerUnit,
                settings.rowWidth * settings.pixelsPerUnit);
            callback(context, settings);

            // if there's a background image specified
        } else if (options.backgroundImage) {

            var img = new Image();
            img.onerror = function () {
                callback(context, settings);
            };
            img.onload = function () {
                context.drawImage(
                    img,
                    (settings.whiteBorder + settings.blackFrameSides) * settings.pixelsPerUnit,
                    (settings.whiteBorder + settings.blackFrameTop) * settings.pixelsPerUnit,
                    settings.rowWidth * settings.pixelsPerUnit,
                    settings.rowWidth * settings.pixelsPerUnit);
                callback(context, settings);
            };
            img.src = options.backgroundImage;

            // neither background image or color specified
        } else {
            callback(context, settings);
        };
    };


    // render the border(s)
    function border(context, settings) {
        context.fillStyle = toColor("1");
        context.fillRect(
            0,
            0,
            settings.units * settings.pixelsPerUnit,
            settings.units * settings.pixelsPerUnit);

        context.fillStyle = toColor("0");
        context.fillRect(
            settings.whiteBorder * settings.pixelsPerUnit,
            settings.whiteBorder * settings.pixelsPerUnit,
            settings.frameHeightAndWidth * settings.pixelsPerUnit,
            settings.frameHeightAndWidth * settings.pixelsPerUnit);
        context.clearRect(
            (settings.whiteBorder + settings.blackFrameSides) * settings.pixelsPerUnit,
            (settings.whiteBorder + settings.blackFrameTop) * settings.pixelsPerUnit,
            settings.rowWidth * settings.pixelsPerUnit,
            (settings.frameHeightAndWidth - settings.blackFrameTop - settings.blackFrameBottom) * settings.pixelsPerUnit);
    };

    // test to see if browser supports canvas
    function canvas() {
        var elem = document.createElement('canvas');
        return !!(elem.getContext && elem.getContext('2d'));
    }

    // do the actual drawing of the tag
    function draw(context, settings, options) {

        // black border
        border(context, settings);

        // spacers that might be hidden
        for (var i = 1; i <= (settings.color ? 4 : 6); i++) {
            spacer(context, settings, i);
        };

        // backbround
        background(context, settings, options, drawComplete);
    };

    function drawComplete(context, settings) {

        // spacers that must show
        spacer(context, settings, 0);
        spacer(context, settings, settings.color ? 5 : 7);

        // render all the damn symbols
        for (var index = 0; index < settings.rows.length; index++) {
            var row = Math.floor(index / (settings.rowCount * 2));
            var col = index - (row * settings.rowCount * 2);
            // looks like custom bw tags have a tail
            if (row < settings.rowCount) {
                symbol(context, settings, settings.rows[index], row, col);
            }
        };
    };

    function drawDot(context, settings, code, row, column) {

        // x = white + black sides 
        var x = (settings.whiteBorder + settings.blackFrameSides)
                + (((column + 1) / 2) * settings.symbolWidth);

        // y = white + black top + first spacer height
        var y = (settings.whiteBorder + settings.blackFrameTop + settings.rowSpacerHeight)
        // row number * (row height + spacer height)
                + (row * (settings.triangleHeight + settings.rowSpacerHeight))
        // 1/3 hi/lo offset to create stagger
                + (settings.triangleHeight * (column % 2 == 0 ? 1 : 2) / 3);

        var r = ((settings.symbolWidth / 2) / 3);

        x = x * settings.pixelsPerUnit;
        y = y * settings.pixelsPerUnit;
        r = r * settings.pixelsPerUnit;

        //draw filled arc/circle
        context.fillStyle = toColor(code);
        context.beginPath();
        context.arc(x, y, r, 0, Math.PI * 2, true);
        context.closePath();
        context.fill();
    }

    function drawPoly(context, poly, fillStyle) {
        context.fillStyle = fillStyle;
        context.beginPath();
        context.moveTo(poly[0], poly[1]);
        for (index = 2; index < poly.length - 1; index += 2) {
            context.lineTo(poly[index], poly[index + 1]);
        }
        context.closePath();
        context.fill();
    }

    function drawTriangle(context, settings, code, row, column) {

        // x = white + black sides 
        var x = (settings.whiteBorder + settings.blackFrameSides)
                + ((column / 2) * settings.symbolWidth);

        // y = white + black top + first spacer height
        var y = (settings.whiteBorder + settings.blackFrameTop + settings.rowSpacerHeight)
        // row number * (row height + spacer height)
                + (row * (settings.triangleHeight + settings.rowSpacerHeight));

        // just some helpers
        x = x * settings.pixelsPerUnit;
        var height = settings.pixelsPerUnit * settings.triangleHeight;
        var width = settings.pixelsPerUnit * settings.symbolWidth;
        y = (y + (column % 2 == 0 ? 0 : settings.triangleHeight)) * settings.pixelsPerUnit;

        // left cap
        if (column == 0) {
            drawPoly(context, [x, y, x, y + height, x + (width / 2), y + height], toColor("0"));
        };

        // symbol
        drawPoly(context, [x, y, x + (width / 2), y + ((column % 2 == 0 ? 1 : -1) * height), x + width, y], toColor(code));

        // right cap
        if (column == settings.symbolsPerRow - 1) {
            drawPoly(context, [x + width, y - height, x + width, y, x + (width / 2), y - height], toColor("0"));
        };
    }

    // parse does the heavy lifting of interpreting
    // the .tag data   
    function parse(data, options) {

        var settings = $.extend({}, baseSettings);

        // first char indicates whether tag is color or b&w
        settings.color = data.charAt(0) == "1";

        // second char indicates number of rows 
        settings.rowCount = parseInt(data.charAt(1));

        // the number of symbols per unit is 
        // always 2 * number of rows
        settings.symbolsPerRow = settings.rowCount * 2;

        // is this a custom tag?
        settings.custom = options.backgroundColor != null || options.backgroundImage != null;

        $.extend(settings, settings.color ? colorSettings : blackAndWhiteSettings);

        // the pitch
        settings.symbolWidth = settings.rowWidth / ((settings.symbolsPerRow / 2) + 0.5);

        settings.pixelsPerUnit = options.width / settings.units;

        // interate through remainder of characters
        // pushing on to rows array        
        settings.rows = [];
        for (var i = 2; i < data.length; i++) {
            var val = toHex(data.charAt(i));
            if (settings.color) {
                // push the "low" code
                settings.rows.push(val.substring(0, 2));
                // push the "high" code
                settings.rows.push(val.substring(2));
            } else {
                // push each char
                settings.rows.push(val.charAt(0));
                settings.rows.push(val.charAt(1));
                settings.rows.push(val.charAt(2));
                settings.rows.push(val.charAt(3));
            }
        }

        return settings;
    };

    // create / prepare canvas context
    function prepCanvas(element, settings) {
        element.html("").append("<canvas></canvas>");
        $context = element.find("canvas");
        $context.attr("height", settings.units * settings.pixelsPerUnit);
        $context.attr("width", settings.units * settings.pixelsPerUnit);
        return $context[0].getContext('2d');
    };

    // the big kahuna ...
    function render(element, options) {

        // get tag data
        var data = $.trim(element.text());

        // parse the tag
        var settings = parse(data, options);

        if (canvas()) {
            // prepare context
            var context = prepCanvas(element, settings);
            // draw the tag
            draw(context, settings, options);
        } else {
            element.html("").html("<span>canvas drawing not supported!</span>");
        }
    };

    // draw white spacer bars side-to-side
    function spacer(context, settings, row) {

        // x = white + black sides - overlay
        var x = settings.whiteBorder + settings.blackFrameSides
            - settings.rowOverlayOverFrameSides;

        // y = white + black sides + (row num * (row height + spacer height))
        var y = settings.whiteBorder + settings.blackFrameTop
            + (row * (settings.triangleHeight + settings.rowSpacerHeight));

        context.fillStyle = toColor("1");
        context.beginPath();
        context.rect(
            x * settings.pixelsPerUnit,
            y * settings.pixelsPerUnit,
            settings.rowSpacerWidth * settings.pixelsPerUnit,
            settings.rowSpacerHeight * settings.pixelsPerUnit);
        context.closePath();
        context.fill();
    };

    // draw symbol
    function symbol(context, settings, code, row, column) {

        if (!settings.custom) {
            drawTriangle(context, settings, code, row, column);
        }
        else {
            drawDot(context, settings, code, row, column);
        };
    };

    // convert coes to colors
    function toColor(code) {
        switch (code) {
            case "0":
                return "#000";
            case "1":
                return "#FFF";
            case "00":
                return "#FF0";
            case "01":
                return "#F0F";
            case "10":
                return "#000";
            case "11":
                return "#0FF";
        };
    };

    // convert hex characters to bin values
    function toHex(ch) {
        var hex = '0123456789ABCDEF';
        var bin =
            ['0000', '0001', '0010',
             '0011', '0100', '0101',
             '0110', '0111', '1000',
             '1001', '1010', '1011',
             '1100', '1101', '1110',
             '1111'];
        return String(bin[hex.indexOf(ch.toUpperCase())]);
    };

    $.tag.defaultOptions = {
        width: 250
    };

})(jQuery)